簡單介紹 useLayoutEffect。
useLayoutEffect 是另一種版本的 useEffect,不同的是他的執行時間是在瀏覽器 repaints 之前。
useLayoutEffect(setup, dependencies?)
setup: 一個 function 放著 effect 的邏輯,跟 useEffect 一樣可以 return 一個 cleanup function。dependencies?: 一個 array,放著 setup function 裡面使用到的外部變數,當元件 re-render 時會使用 Object.is() 一個一個做比較,如果其中一個是 false 的時候會執行 clean up function 然後再執行 setup function。
useLayout Effect 可以想成是 useEffect 的一種變體,只是他執行的時間跟 useEffect 不同,所以就直接來看 code 吧,一樣從執行的時間開始看起。
import { useState, useEffect, useLayoutEffect } from "react";
function App() {
  const [count, setCount] = useState(0);
  useLayoutEffect(() => {
    console.log("useLayoutEffect");
    return () => {
      console.log("useLayoutEffect-return");
    };
  });
  useEffect(() => {
    console.log("useEffect");
    return () => {
      console.log("useEffect-return");
    };
  });
  const onClick = () => setCount(count + 1);
  return (
    <div>
      <h1>useLayoutEffect</h1>
      <div>Count: {count}</div>
      <button onClick={onClick}>add count</button>
    </div>
  );
}
結果。

初次 render: useLayoutEffect -> useEffect
re-render: useLayoutEffect-cleanup -> useLayoutEffect -> useEffect-cleanup -> useEffect
上面有說到盡量減少不要使用到 useLayoutEffect 會影響效能,那接著就來看看什麼情況下會需要用到 useLayoutEffect 吧。
假設有一個 content 的 div,元素的高度是未定的,可能會因為用戶的裝置導致元素高度不定。
但是我希望我的 popup 的 div 可以永遠都出現在 content 的正下方,所以在 content 出現之前我的 popup 的位置是沒有辦法確定的,必須要透過 useEffect 來另外設定 popup 的位置。
import { useRef, useState, useEffect } from "react";
function App() {
  const [isShow, setIsShow] = useState(false);
  const [top, setTop] = useState(0);
  const [height, setHeight] = useState(0);
  const divRef = useRef<HTMLDivElement>(null);
  useEffect(() => {
    // 使用 useEffect
    if (isShow && divRef.current) {
      // 取得元素位置跟高度,確認 popup 的位置
      setTop(divRef.current.offsetHeight + divRef.current.offsetTop);
    }
  }, [isShow]);
  function handleClick() {
    setIsShow(!isShow);
    if (!isShow) {
      // content 元素出現的時候高度未知
      setHeight(Math.random() * 50 + 20);
    }
    if (isShow) {
      // content 元素隱藏的時候把重置位置
      setTop(0);
    }
  }
  return (
    <div>
      <button onClick={handleClick}>Show Popup</button>
      {isShow && (
        <>
          <div
            ref={divRef}
            style={{
              height: `${height}px`,
              border: "solid 1px black",
            }}
          >
            content
          </div>
          <div
            style={{
              position: "absolute",
              width: "100%",
              backgroundColor: "#f00",
              top: `${top}px`,
            }}
          >
            Popup
          </div>
        </>
      )}
    </div>
  );
}

會發現 pop 的位置會出現不正常的跳動,因為 useEffect 在執行的時候瀏覽器已經執行了 repaint 的動作,所以元素已經出現在畫面上了,才又更新元素的位置,使用 useLayoutEffect 就可以避免這個元素跳動的問題。
useLayoutEffect(() => {
  // 改用 useLayoutEffect
  if (isShow && divRef.current) {
    setTop(divRef.current.offsetHeight + divRef.current.offsetTop);
  }
}, [isShow]);

因為 useLayoutEffect 執行的時間是在瀏覽器完成 reflow 之後 repaint 之前,這個時候已經知道元素位置跟大小,所以就可以在這個時候設定 popup 的位置,然後才執行 repaint,所以就可以避免元素的不自然跳動。
下一篇簡單介紹 custom hook。
如果內容有誤再麻煩大家指教,我會盡快修改。
這個系列的文章會同步更新在我個人的 Medium,歡迎大家來看看 👋👋👋
Medium